/*
    Copyright © 2014-2019 by The qTox Project Contributors

    This file is part of qTox, a Qt-based graphical interface for Tox.

    qTox is libre software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    qTox is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with qTox.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "openal.h"

#include "audio/iaudiosettings.h"
#include <algorithm>
#include <thread>
#include <iostream>
#include <fstream>
#include <cassert>


namespace {
void applyGain(int16_t* buffer, uint32_t bufferSize, double gainFactor)
{
    for (uint32_t i = 0; i < bufferSize; ++i) {
        // gain amplification with clipping to 16-bit boundaries
        buffer[i] =
            Bound<int16_t>(std::numeric_limits<int16_t>::min(), std::round(buffer[i] * gainFactor),
                            std::numeric_limits<int16_t>::max());
    }
}
} // namespace

/**
 * @class OpenAL
 * @brief Provides the OpenAL audio backend
 *
 * @var BUFFER_COUNT
 * @brief Number of buffers to use per audio source
 *
 * @var AUDIO_CHANNELS
 * @brief Ideally, we'd auto-detect, but that's a sane default
 */

static const unsigned int BUFFER_COUNT = 16;
static const uint32_t AUDIO_CHANNELS = 2;
constexpr double OpenAL::minInGain;
constexpr double OpenAL::maxInGain;

OpenAL::OpenAL(IAudioSettings& _settings)
    : settings{_settings}
    , audioThread{new std::thread}
{
    // initialize OpenAL error stack
    //alGetError();
    //alcGetError(nullptr);

    //audioThread->setObjectName("qTox Audio");
    //QObject::connect(audioThread, &std::thread::finished, &voiceTimer, &QTimer::stop);
    //QObject::connect(audioThread, &std::thread::finished, &captureTimer, &QTimer::stop);
    //QObject::connect(audioThread, &std::thread::finished, audioThread, &std::thread::deleteLater);

    //moveToThread(audioThread);

    //voiceTimer.setSingleShot(true);
    //voiceTimer.moveToThread(audioThread);
    //connect(this, &OpenAL::startActive, &voiceTimer,
    //        static_cast<void (QTimer::*)(int)>(&QTimer::start));
    //connect(&voiceTimer, &QTimer::timeout, this, &OpenAL::stopActive);

    //connect(&captureTimer, &QTimer::timeout, this, &OpenAL::doAudio);
    //captureTimer.setInterval(AUDIO_FRAME_DURATION / 2);
    //captureTimer.setSingleShot(false);
    //captureTimer.moveToThread(audioThread);
    //// TODO for Qt 5.6+: use qOverload
    //connect(audioThread, &std::thread::started, &captureTimer,
    //        static_cast<void (QTimer::*)(void)>(&QTimer::start));

    //cleanupTimer.setInterval(1000);
    //cleanupTimer.setSingleShot(false);
    //connect(&cleanupTimer, &QTimer::timeout, this, &OpenAL::cleanupSound);
    //// TODO for Qt 5.6+: use qOverload
    //connect(audioThread, &std::thread::started, &cleanupTimer,
    //        static_cast<void (QTimer::*)(void)>(&QTimer::start));

    //audioThread->start();
}

OpenAL::~OpenAL()
{
    //audioThread->exit();
    //audioThread->wait();
    cleanupInput();
    cleanupOutput();
}

void OpenAL::checkAlError() noexcept
{

}



/**
 * @brief Returns the current output volume (between 0 and 1)
 */
double OpenAL::outputVolume() 
{
    //QMutexLocker locker(&audioLock);
    std::lock_guard<std::mutex> locker(audioLock);

    double volume = 0.0;


    return volume;
}

/**
 * @brief Set the master output volume.
 *
 * @param[in] volume   the master volume (between 0 and 1)
 */
void OpenAL::setOutputVolume(double volume)
{
    std::lock_guard<std::mutex> locker(audioLock);

    volume = std::max(0.0, std::min(volume, 1.0));

    checkAlError();
}

/**
 * @brief The minimum gain value for an input device.
 *
 * @return minimum gain value in dB
 */
double OpenAL::minInputGain() 
{
    std::lock_guard<std::mutex> locker(audioLock);
    return minInGain;
}

/**
 * @brief The maximum gain value for an input device.
 *
 * @return maximum gain value in dB
 */
double OpenAL::maxInputGain() 
{
    std::lock_guard<std::mutex> locker(audioLock);
    return maxInGain;
}

/**
 * @brief The minimum threshold value for an input device.
 *
 * @return minimum normalized threshold
 */
double OpenAL::minInputThreshold() 
{
    std::lock_guard<std::mutex> locker(audioLock);
    return minInThreshold;
}

/**
 * @brief The maximum normalized threshold value for an input device.
 *
 * @return maximum normalized threshold
 */
double OpenAL::maxInputThreshold() 
{
    std::lock_guard<std::mutex> locker(audioLock);
    return maxInThreshold;
}

void OpenAL::reinitInput(const std::string& inDevDesc)
{
    std::unique_lock <std::mutex> locker(audioLock);

    const auto bakSources = sources;
    sources.clear();

    cleanupInput();
    initInput(inDevDesc);

    locker.unlock();
    // this must happen outside `audioLock`, to avoid a deadlock when
    // a slot on AlSource::invalidate tries to create a new source immedeately.
    for (auto& source : bakSources) {
        source->kill();
    }
}

bool OpenAL::reinitOutput(const std::string& outDevDesc)
{
    std::unique_lock<std::mutex> locker(audioLock);

    const auto bakSinks = sinks;

    sinks.clear();

    cleanupOutput();
    const bool result = initOutput(outDevDesc);

    locker.unlock();
    // this must happen outside `audioLock`, to avoid a deadlock when
    // a slot on AlSink::invalidate tries to create a new source immedeately.
    for (auto& sink : bakSinks) {
        sink->kill();
    }

    return result;
}

/**
 * @brief Allocates ressources for a new audio output
 * @return AudioSink on success, nullptr on failure
 */
std::unique_ptr<IAudioSink> OpenAL::makeSink()
{
    std::lock_guard<std::mutex> locker(audioLock);

    if (!autoInitOutput()) {
        std::cout<<"Failed to subscribe to audio output device." << std::endl;
        return {};
    }



    auto const sink = nullptr;
    if (sink == nullptr) {
        return {};
    }

    sinks.insert(sink);


    return std::unique_ptr<IAudioSink>{sink};
}

/**
 * @brief Must be called by the destructor of AlSink to remove the internal ressources.
 *        If no sinks are opened, the output is closed afterwards.
 * @param sink Audio sink to remove.
 */
void OpenAL::destroySink(AlSink& sink)
{
    std::lock_guard<std::mutex> locker(audioLock);

    const auto sinksErased = sinks.erase(&sink);
    const auto soundSinksErased = soundSinks.erase(&sink);

    if (sinksErased == 0 && soundSinksErased == 0) {
        std::cout << "Destroying non-existent sink" << std::endl;
        return;
    }

    const uint32_t sid = sink.getSourceId();


    if (sinks.empty() && soundSinks.empty()) {
        cleanupOutput();
    }
}

/**
 * @brief Subscribe to capture sound from the opened input device.
 *
 * If the input device is not open, it will be opened before capturing.
 */
std::unique_ptr<IAudioSource> OpenAL::makeSource()
{
    std::lock_guard<std::mutex> locker(audioLock);

    if (!autoInitInput()) {
        std::cout<<"Failed to subscribe to audio input device." << std::endl;
        return {};
    }

    auto const source = new AlSource(*this);
    if (source == nullptr) {
        return {};
    }

    sources.insert(source);

    std::cout << "Subscribed to audio input device [" << sources.size() << "subscriptions ]" << std::endl;

    return std::unique_ptr<IAudioSource>{source};
}

/**
 * @brief Unsubscribe from capturing from an opened input device.
 *
 * If the input device has no more subscriptions, it will be closed.
 */
void OpenAL::destroySource(AlSource& source)
{
    std::lock_guard<std::mutex> locker(audioLock);

    const auto s = sources.find(&source);
    if (s == sources.end()) {
        std::cout << "Destroyed non-existent source" << std::endl;
        return;
    }

    sources.erase(s);

    std::cout << "Unsubscribed from audio input device [" << sources.size() << "subscriptions left ]" << std::endl;

    if (sources.empty()) {
        cleanupInput();
    }
}

/**
 * @brief Initialize audio input device, if not initialized.
 *
 * @return true, if device was initialized; false otherwise
 */
bool OpenAL::autoInitInput()
{
    return true;
}

/**
 * @brief Initialize audio output device, if not initialized.
 *
 * @return true, if device was initialized; false otherwise
 */
bool OpenAL::autoInitOutput()
{
    return  true ;
}

bool OpenAL::initInput(const std::string& deviceName)
{
    return initInput(deviceName, AUDIO_CHANNELS);
}

bool OpenAL::initInput(const std::string& deviceName, uint32_t channels)
{
    if (!settings.getAudioInDevEnabled()) {
        return false;
    }

    std::cout << "Opening audio input" << deviceName << std::endl;
 

    // TODO: Try to actually detect if our audio source is stereo
    this->channels = channels;
    int stereoFlag = channels == 1;
    const int bytesPerSample = 2;
    const int safetyFactor = 2; // internal OpenAL ring buffer. must be larger than our inputBuffer
                                // to avoid the ring from overwriting itself between captures.
    AUDIO_FRAME_SAMPLE_COUNT_TOTAL = AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL * channels;



    inputBuffer = new int16_t[AUDIO_FRAME_SAMPLE_COUNT_TOTAL];
    setInputGain(settings.getAudioInGainDecibel());
    setInputThreshold(settings.getAudioThreshold());


    return true;
}

/**
 * @brief Open an audio output device
 */
bool OpenAL::initOutput(const std::string& deviceName)
{
    // there should be no sinks when initializing the output
    assert(sinks.size() == 0);

    outputInitialized = false;
    if (!settings.getAudioOutDevEnabled())
        return false;

    std::cout << "Opening audio output" << deviceName << std::endl;

    checkAlError();

    outputInitialized = true;
    return true;
}




/**
 * @brief Play a 48kHz mono 16bit PCM sound
 */
void OpenAL::playMono16Sound(AlSink& sink, const IAudioSink::Sound& sound)
{
    const uint32_t sourceId = sink.getSourceId();
    std::string strpath(IAudioSink::getSound(sound));

    if (!is_file_exist(strpath.c_str())) {
        std::cout << "Trying to open non-existent sound file" << std::endl;
        return;
    }
    strpath.c_str();
    std::ifstream infile;
    infile.open(strpath.c_str(), std::ios::in);
    std::string data((std::istreambuf_iterator<char>(infile)),std::istreambuf_iterator<char>());

    if (data.size()<=0) {
        std::cout << "Sound file contained no data" << std::endl;
        return;
    }

    soundSinks.insert(&sink);
}

void OpenAL::cleanupSound()
{
    std::lock_guard<std::mutex> locker(audioLock);

    auto sinkIt = soundSinks.begin();
    while (sinkIt != soundSinks.end()) {
        auto sink = *sinkIt;
    }
}

void OpenAL::playAudioBuffer(uint32_t sourceId, const int16_t* data, int samples, unsigned channels,
                             int sampleRate)
{
    assert(channels == 1 || channels == 2);
    std::lock_guard<std::mutex> locker(audioLock);
}

/**
 * @brief Close active audio input device.
 */
void OpenAL::cleanupInput()
{


    delete[] inputBuffer;
}

/**
 * @brief Close active audio output device
 */
void OpenAL::cleanupOutput()
{
    outputInitialized = false;

}

/**
 * @brief Called by doCapture to calculate volume of the audio buffer
 *
 * @param[in] buf   the current audio buffer
 *
 * @return normalized volume between 0-1
 */
float OpenAL::getVolume()
{
    const uint32_t samples = AUDIO_FRAME_SAMPLE_COUNT_TOTAL;
    const float rootTwo = 1.414213562; // sqrt(2), but sqrt is not constexpr
    // calculate volume as the root mean squared of amplitudes in the sample
    float sumOfSquares = 0;
    for (uint32_t i = 0; i < samples; i++) {
        float sample = static_cast<float>(inputBuffer[i]) / std::numeric_limits<int16_t>::max();
        sumOfSquares += std::pow(sample, 2);
    }
    const float rms = std::sqrt(sumOfSquares / samples);
    // our calculated normalized volume could possibly be above 1 because our RMS assumes a sinusoidal wave
    const float normalizedVolume = std::min(rms * rootTwo, 1.0f);
    return normalizedVolume;
}

/**
 * @brief Called by voiceTimer's timeout to disable audio broadcasting
 */
void OpenAL::stopActive()
{
    isActive = false;
}

/**
 * @brief handles recording of audio frames
 */
void OpenAL::doInput()
{
    applyGain(inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_TOTAL, gainFactor);

    float volume = getVolume();
    if (volume >= inputThreshold) {
        isActive = true;
        startActive(voiceHold);
    } else if (!isActive) {
        volume = 0;
    }

    // NOTE(sudden6): this loop probably doesn't scale too well with many sources
    for (auto source : sources) {
        //emit source->volumeAvailable(volume);
    }
    if (!isActive) {
        return;
    }

    // NOTE(sudden6): this loop probably doesn't scale too well with many sources
    for (auto source : sources) {
        //emit source->frameAvailable(inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL, channels,
                                    //AUDIO_SAMPLE_RATE);
    }
}

void OpenAL::doOutput()
{
    // Nothing
}

/**
 * @brief Called on the captureTimer events to capture audio
 */
void OpenAL::doAudio()
{
    std::lock_guard<std::mutex> locker(audioLock);

}


/**
 * @brief Returns true if the output device is open
 */
bool OpenAL::isOutputReady() 
{
    return true;
}

std::list<std::string> OpenAL::outDeviceNames()
{
    return {};
}

std::list<std::string> OpenAL::inDeviceNames()
{
    std::list<std::string> list;

    return list;
}

/**
 * @brief Free all buffers that finished playing on a source
 * @param sourceId where to remove the buffers from
 */
void OpenAL::cleanupBuffers(uint32_t sourceId)
{
    // unqueue all buffers from the source

}

void OpenAL::startLoop(uint32_t sourceId)
{
}

void OpenAL::stopLoop(uint32_t sourceId)
{
    cleanupBuffers(sourceId);
}

double OpenAL::inputGain() const
{
    return gain;
}

double OpenAL::getInputThreshold() 
{
    return inputThreshold;
}

double OpenAL::inputGainFactor() const
{
    return gainFactor;
}

void OpenAL::setInputGain(double dB)
{
    gain = Bound(minInGain, dB, maxInGain);
    gainFactor = std::pow(10.0, (gain / 20.0));
}

void OpenAL::setInputThreshold(double normalizedThreshold)
{
    inputThreshold = normalizedThreshold;
}
